using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

namespace Ultima
{
    public sealed class FileIndex
    {
        public Entry3D[] Index { get; private set; }
        public Stream Stream { get; private set; }
        public long IdxLength { get; private set; }
        private string MulPath;

        public Stream Seek(int index, out int length, out int extra, out bool patched)
        {
            if (index < 0 || index >= Index.Length)
            {
                length = extra = 0;
                patched = false;
                return null;
            }

            Entry3D e = Index[index];

            if (e.lookup < 0)
            {
                length = extra = 0;
                patched = false;
                return null;
            }

            length = e.length & 0x7FFFFFFF;
            extra = e.extra;

            if ((e.length & (1 << 31)) != 0)
            {
                patched = true;
                Verdata.Seek(e.lookup);
                return Verdata.Stream;
            }

            if (e.length < 0)
            {
                length = extra = 0;
                patched = false;
                return null;
            }

            if ((Stream == null) || (!Stream.CanRead) || (!Stream.CanSeek))
            {
                if (MulPath == null)
                    Stream = null;
                else
                    Stream = new FileStream(MulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            }

            if (Stream == null)
            {
                length = extra = 0;
                patched = false;
                return null;
            }
            else if (Stream.Length < e.lookup)
            {
                length = extra = 0;
                patched = false;
                return null;
            }

            patched = false;

            Stream.Seek(e.lookup, SeekOrigin.Begin);
            return Stream;
        }

        public bool Valid(int index, out int length, out int extra, out bool patched)
        {
            if (index < 0 || index >= Index.Length)
            {
                length = extra = 0;
                patched = false;
                return false;
            }

            Entry3D e = Index[index];

            if (e.lookup < 0)
            {
                length = extra = 0;
                patched = false;
                return false;
            }

            length = e.length & 0x7FFFFFFF;
            extra = e.extra;

            if ((e.length & (1 << 31)) != 0)
            {
                patched = true;
                return true;
            }

            if (e.length < 0)
            {
                length = extra = 0;
                patched = false;
                return false;
            }

            if ((MulPath == null) || !File.Exists(MulPath))
            {
                length = extra = 0;
                patched = false;
                return false;
            }

            if ((Stream == null) || (!Stream.CanRead) || (!Stream.CanSeek))
            {
                Stream = new FileStream(MulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            }

            if (Stream.Length < e.lookup)
            {
                length = extra = 0;
                patched = false;
                return false;
            }

            patched = false;

            return true;
        }

        public FileIndex(string idxFile, string mulFile, int length, int file) : this(idxFile, mulFile, length, file, ".dat", -1, false)
        {
        }

        public FileIndex(string idxFile, string mulFile, int length, int file, string uopEntryExtension, int idxLength, bool hasExtra)
        {
            Index = new Entry3D[length];

            string idxPath = null;
            MulPath = null;

            if (Files.MulPath == null)
                Files.LoadMulPath();

            if (Files.MulPath.Count > 0)
            {
                idxPath = Files.MulPath[idxFile.ToLower()];
                MulPath = Files.MulPath[mulFile.ToLower()];

                if (String.IsNullOrEmpty(idxPath))
                {
                    idxPath = null;
                }
                else
                {
                    if (String.IsNullOrEmpty(Path.GetDirectoryName(idxPath)))
                        idxPath = Path.Combine(Files.RootDir, idxPath);

                    if (!File.Exists(idxPath))
                        idxPath = null;
                }

                if (String.IsNullOrEmpty(MulPath))
                {
                    MulPath = null;
                }
                else
                {
                    if (String.IsNullOrEmpty(Path.GetDirectoryName(MulPath)))
                        MulPath = Path.Combine(Files.RootDir, MulPath);

                    if (!File.Exists(MulPath))
                        MulPath = null;
                }
            }

            /* UOP files support code, written by Wyatt (c) www.ruosi.org
             * idxLength variable was added for compatibility with legacy code for art (see art.cs)
             * At the moment the only UOP file having entries with extra field is gumpartlegacy.uop,
             * and it's two dwords in the beginning of the entry.
             * It's possible that UOP can include some entries with unknown hash: not really unknown for me, but
             * not useful for reading legacy entries. That's why i removed unknown hash exception throwing from this code
             */
            if (MulPath != null && MulPath.EndsWith(".uop"))
            {
                using (FileStream index = new FileStream(MulPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    Stream = new FileStream(MulPath, FileMode.Open, FileAccess.Read, FileShare.Read);

                    FileInfo fi = new FileInfo(MulPath);
                    string uopPattern = fi.Name.Replace(fi.Extension, "").ToLowerInvariant();

                    using (BinaryReader br = new BinaryReader(Stream))
                    {
                        br.BaseStream.Seek(0, SeekOrigin.Begin);

                        if (br.ReadInt32() != 0x50594D)
                            throw new ArgumentException("Bad UOP file.");

                        br.ReadInt64(); // version + signature
                        long nextBlock = br.ReadInt64();
                        br.ReadInt32(); // block capacity
                        int count = br.ReadInt32();

                        if (idxLength > 0)
                            IdxLength = idxLength * 12;

                        Dictionary<ulong, int> hashes = new Dictionary<ulong, int>();

                        for (int i = 0; i < length; i++)
                        {
                            string entryName = string.Format("build/{0}/{1:D8}{2}", uopPattern, i, uopEntryExtension);
                            ulong hash = HashFileName(entryName);

                            if (!hashes.ContainsKey(hash))
                                hashes.Add(hash, i);
                        }

                        br.BaseStream.Seek(nextBlock, SeekOrigin.Begin);

                        do
                        {
                            int filesCount = br.ReadInt32();
                            nextBlock = br.ReadInt64();

                            for (int i = 0; i < filesCount; i++)
                            {
                                long offset = br.ReadInt64();
                                int headerLength = br.ReadInt32();
                                int compressedLength = br.ReadInt32();
                                int decompressedLength = br.ReadInt32();
                                ulong hash = br.ReadUInt64();
                                br.ReadUInt32(); // Adler32
                                short flag = br.ReadInt16();

                                int entryLength = flag == 1 ? compressedLength : decompressedLength;

                                if (offset == 0)
                                    continue;

                                int idx;
                                if (hashes.TryGetValue(hash, out idx))
                                {
                                    if (idx < 0 || idx > Index.Length)
                                        throw new IndexOutOfRangeException("hashes dictionary and files collection have different count of entries!");

                                    Index[idx].lookup = (int)(offset + headerLength);
                                    Index[idx].length = entryLength;

                                    if (hasExtra)
                                    {
                                        long curPos = br.BaseStream.Position;

                                        br.BaseStream.Seek(offset + headerLength, SeekOrigin.Begin);

                                        byte[] extra = br.ReadBytes(8);

                                        short extra1 = (short)((extra[3] << 24) | (extra[2] << 16) | (extra[1] << 8) | extra[0]);
                                        short extra2 = (short)((extra[7] << 24) | (extra[6] << 16) | (extra[5] << 8) | extra[4]);

                                        Index[idx].lookup += 8;
                                        Index[idx].extra = extra1 << 16 | extra2;

                                        br.BaseStream.Seek(curPos, SeekOrigin.Begin);
                                    }
                                }
                            }
                        }
                        while (br.BaseStream.Seek(nextBlock, SeekOrigin.Begin) != 0);
                    }
                }
            }
            else if ((idxPath != null) && (MulPath != null))
            {
                using (FileStream index = new FileStream(idxPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    Stream = new FileStream(MulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
                    int count = (int)(index.Length / 12);
                    IdxLength = index.Length;
                    GCHandle gc = GCHandle.Alloc(Index, GCHandleType.Pinned);
                    byte[] buffer = new byte[index.Length];
                    index.Read(buffer, 0, (int)index.Length);
                    Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)Math.Min(IdxLength, length * 12));
                    gc.Free();
                    for (int i = count; i < length; ++i)
                    {
                        Index[i].lookup = -1;
                        Index[i].length = -1;
                        Index[i].extra = -1;
                    }
                }
            }
            else
            {
                Stream = null;
                return;
            }

            Entry5D[] patches = Verdata.Patches;

            if (file > -1)
            {
                for (int i = 0; i < patches.Length; ++i)
                {
                    Entry5D patch = patches[i];

                    if (patch.file == file && patch.index >= 0 && patch.index < length)
                    {
                        Index[patch.index].lookup = patch.lookup;
                        Index[patch.index].length = patch.length | (1 << 31);
                        Index[patch.index].extra = patch.extra;
                    }
                }
            }
        }

        public FileIndex(string idxFile, string mulFile, int file)
        {
            string idxPath = null;
            MulPath = null;
            if (Files.MulPath == null)
                Files.LoadMulPath();
            if (Files.MulPath.Count > 0)
            {
                idxPath = Files.MulPath[idxFile.ToLower()];
                MulPath = Files.MulPath[mulFile.ToLower()];
                if (String.IsNullOrEmpty(idxPath))
                    idxPath = null;
                else
                {
                    if (String.IsNullOrEmpty(Path.GetDirectoryName(idxPath)))
                        idxPath = Path.Combine(Files.RootDir, idxPath);
                    if (!File.Exists(idxPath))
                        idxPath = null;
                }
                if (String.IsNullOrEmpty(MulPath))
                    MulPath = null;
                else
                {
                    if (String.IsNullOrEmpty(Path.GetDirectoryName(MulPath)))
                        MulPath = Path.Combine(Files.RootDir, MulPath);
                    if (!File.Exists(MulPath))
                        MulPath = null;
                }
            }

            if ((idxPath != null) && (MulPath != null))
            {
                using (FileStream index = new FileStream(idxPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    Stream = new FileStream(MulPath, FileMode.Open, FileAccess.Read, FileShare.Read);
                    int count = (int)(index.Length / 12);
                    IdxLength = index.Length;
                    Index = new Entry3D[count];
                    GCHandle gc = GCHandle.Alloc(Index, GCHandleType.Pinned);
                    byte[] buffer = new byte[index.Length];
                    index.Read(buffer, 0, (int)index.Length);
                    Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)index.Length);
                    gc.Free();
                }
            }
            else
            {
                Stream = null;
                Index = new Entry3D[1];
                return;
            }
            Entry5D[] patches = Verdata.Patches;

            if (file > -1)
            {
                for (int i = 0; i < patches.Length; ++i)
                {
                    Entry5D patch = patches[i];

                    if (patch.file == file && patch.index >= 0 && patch.index < Index.Length)
                    {
                        Index[patch.index].lookup = patch.lookup;
                        Index[patch.index].length = patch.length | (1 << 31);
                        Index[patch.index].extra = patch.extra;
                    }
                }
            }
        }

        /// <summary>
        /// Method for calculating entry hash by it's name.
        /// Taken from Mythic.Package.dll
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public static ulong HashFileName(string s)
        {
            uint eax, ecx, edx, ebx, esi, edi;

            eax = ecx = edx = ebx = esi = edi = 0;
            ebx = edi = esi = (uint)s.Length + 0xDEADBEEF;

            int i = 0;

            for (i = 0; i + 12 < s.Length; i += 12)
            {
                edi = (uint)((s[i + 7] << 24) | (s[i + 6] << 16) | (s[i + 5] << 8) | s[i + 4]) + edi;
                esi = (uint)((s[i + 11] << 24) | (s[i + 10] << 16) | (s[i + 9] << 8) | s[i + 8]) + esi;
                edx = (uint)((s[i + 3] << 24) | (s[i + 2] << 16) | (s[i + 1] << 8) | s[i]) - esi;

                edx = (edx + ebx) ^ (esi >> 28) ^ (esi << 4);
                esi += edi;
                edi = (edi - edx) ^ (edx >> 26) ^ (edx << 6);
                edx += esi;
                esi = (esi - edi) ^ (edi >> 24) ^ (edi << 8);
                edi += edx;
                ebx = (edx - esi) ^ (esi >> 16) ^ (esi << 16);
                esi += edi;
                edi = (edi - ebx) ^ (ebx >> 13) ^ (ebx << 19);
                ebx += esi;
                esi = (esi - edi) ^ (edi >> 28) ^ (edi << 4);
                edi += ebx;
            }

            if (s.Length - i > 0)
            {
                switch (s.Length - i)
                {
                    case 12:
                        esi += (uint)s[i + 11] << 24;
                        goto case 11;
                    case 11:
                        esi += (uint)s[i + 10] << 16;
                        goto case 10;
                    case 10:
                        esi += (uint)s[i + 9] << 8;
                        goto case 9;
                    case 9:
                        esi += (uint)s[i + 8];
                        goto case 8;
                    case 8:
                        edi += (uint)s[i + 7] << 24;
                        goto case 7;
                    case 7:
                        edi += (uint)s[i + 6] << 16;
                        goto case 6;
                    case 6:
                        edi += (uint)s[i + 5] << 8;
                        goto case 5;
                    case 5:
                        edi += (uint)s[i + 4];
                        goto case 4;
                    case 4:
                        ebx += (uint)s[i + 3] << 24;
                        goto case 3;
                    case 3:
                        ebx += (uint)s[i + 2] << 16;
                        goto case 2;
                    case 2:
                        ebx += (uint)s[i + 1] << 8;
                        goto case 1;
                    case 1:
                        ebx += (uint)s[i];
                        break;
                }

                esi = (esi ^ edi) - ((edi >> 18) ^ (edi << 14));
                ecx = (esi ^ ebx) - ((esi >> 21) ^ (esi << 11));
                edi = (edi ^ ecx) - ((ecx >> 7) ^ (ecx << 25));
                esi = (esi ^ edi) - ((edi >> 16) ^ (edi << 16));
                edx = (esi ^ ecx) - ((esi >> 28) ^ (esi << 4));
                edi = (edi ^ edx) - ((edx >> 18) ^ (edx << 14));
                eax = (esi ^ edi) - ((edi >> 8) ^ (edi << 24));

                return ((ulong)edi << 32) | eax;
            }

            return ((ulong)esi << 32) | eax;
        }

    }

    [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1)]
    public struct Entry3D
    {
        public int lookup;
        public int length;
        public int extra;
    }
}